<!-- This Source Code Form is subject to the terms of the Mozilla Public
     License, v. 2.0. If a copy of the MPL was not distributed with this
     file, You can obtain one at https://mozilla.org/MPL/2.0/. -->
<template>
    <div class="frame-animator-panel">
        <div v-if="light" class="frame-animator-light">
            <span class="btn btn-secondary" @click="openAnimationEditor"><i class="fas fa-film"></i> Open Animation Editor</span>
            <span class="frame-animator-title">{{framePlayer.name}}</span>
        </div>

        <div v-else class="frame-animator-container">
            <div class="frame-animator-header">
                <div class="frame-animator-title">
                    <i class="fas fa-film"></i>
                    <h3>{{framePlayer.name}}</h3>
                </div>
                <div class="frame-animator-controls">
                    <div class="frame-animator-player-buttons">
                        <div v-if="!isRecording">
                            <span class="btn btn-danger btn-small"
                                @click="startRecording"
                                title="Record items for currently selected frame"
                                >
                                <i class="far fa-dot-circle"></i>
                            </span>
                            <span class="btn btn-secondary btn-small" title="Previous" @click="moveFrameLeft"><i class="fas fa-angle-left"></i></span>
                            <span v-if="isPlaying" class="btn btn-secondary btn-small" title="Stop" @click="stopAnimations"><i class="fas fa-stop"></i></span>
                            <span v-else class="btn btn-secondary btn-small" title="Play" @click="playAnimations"><i class="fas fa-play"></i></span>
                            <span class="btn btn-secondary btn-small" title="Next" @click="moveFrameRight"><i class="fas fa-angle-right"></i></span>
                        </div>
                        <div v-else>
                            <span class="btn btn-danger btn-small"
                                @click="stopRecording"
                                title="Record items for currently selected frame"
                                >
                                <i class="fas fa-stop"></i> Stop recording
                            </span>
                        </div>
                    </div>
                    <div class="frame-animator-frame-input" v-if="!isRecording && selectedFrameControl.trackIdx >= 0 && selectedFrameControl.frame >= 0">
                        <div v-if="!selectedFrameControl.blank && selectedFrameControl.propertyDescriptor">
                            <PropertyInput
                                    :key="`frame-prop-input-${selectedFrameControl.trackIdx}-${selectedFrameControl.frame}`"
                                    :editorId="editorId"
                                    :descriptor="selectedFrameControl.propertyDescriptor"
                                    :value="selectedFrameControl.value"
                                    :shapeProps="{}"
                                    :schemeContainer="schemeContainer"
                                    :itemId="null"
                                    @input="onFramePropertyInput" />

                        </div>
                    </div>

                </div>
                <div class="frame-animator-right-panel">
                    <span class="icon" @click="$emit('close')"><i class="fas fa-times"/></span>
                </div>
            </div>

            <div ref="frameAnimatorCanvas" class="frame-animator-canvas">
                <table class="frame-animator-matrix">
                    <thead>
                        <tr :class="{'drop-below': trackDrag.on && trackDrag.dropHead}">
                            <th class="frame-animator-buttons">
                                <span class="btn btn-small" @click="toggleAddTrackModal()">Add track</span>
                            </th>
                            <th v-for="frame in totalFrames"
                                @click="selectFrame(frame)"
                                :title="frame"
                                :class="{current: frame === currentFrame}"
                                >
                                {{frame}}
                            </th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr v-for="(track, trackIdx) in framesMatrix"
                            class="track-droppable-area"
                            :data-track-index="trackIdx"
                            :class="{'selected-track': trackIdx === selectedTrackIdx, 'track-missing': !track.propertyDescriptor && track.kind !== 'function-header' && track.kind !== 'function', 'drop-below': trackDrag.on && !trackDrag.dropHead && trackIdx === trackDrag.dstTrackIdx}"
                            :style="{'background-color': track.color}"
                            >
                            <td class="frame-animator-property"
                                @mousedown="onTrackLabelMouseDown(trackIdx, $event)"
                                :class="['frame-animator-property-'+track.kind, track.kind === 'item' || track.kind === 'function-header'? 'draggable': null]"
                                :colspan="(track.kind === 'function-header' || track.kind === 'panel-buttons') ? totalFrames + 1 : 1"
                                >
                                <div v-if="track.kind === 'item'">
                                    <span v-if="track.itemName" class="frame-item-name">{{track.itemName}}</span>
                                    <span v-else class="frame-item-name">Deleted item</span>
                                    <span class="frame-item-property" v-if="track.propertyDescriptor && track.propertyDescriptor.name">{{track.propertyDescriptor.name}}</span>
                                    <span class="frame-item-property" v-else>{{track.property}}</span>
                                </div>
                                <div v-else-if="track.kind === 'sections'">
                                    <i class="fas fa-paragraph"></i> Sections
                                </div>
                                <div class="function-animator-track-header" v-else-if="track.kind === 'function-header'">
                                    {{track.name}}
                                </div>
                                <div v-else-if="track.kind === 'function'">
                                    {{track.property}}
                                </div>
                                <div v-else-if="track.kind === 'scheme'">
                                    {{track.property}}
                                </div>
                                <div v-else-if="track.kind === 'panel-buttons'">
                                    <span class="btn btn-small" @click="toggleAddTrackModal()">Add track</span>
                                </div>

                                <div class="frame-property-operations" v-if="track.kind !== 'panel-buttons'">
                                    <span v-if="track.kind === 'function-header'" class="toggle-button" title="Edit function" @click="toggleEditFunctionArgumentsForTrack(track)"><i class="fas fa-cog"></i></span>
                                    <span v-if="track.kind !== 'sections' && track.kind !== 'function'" class="toggle-button" title="Remove animation track" @click="removeAnimationTrack(track)"><i class="fas fa-trash"></i></span>
                                </div>
                            </td>
                            <td v-for="(frame, frameIdx) in track.frames"
                                class="frame frame-droppable-area"
                                :data-track-idx="trackIdx"
                                :data-frame-idx="frameIdx"
                                :class="{active: !frame.blank, current: frame.frame === currentFrame, 'drop-candidate': frameDrag.source.trackIdx === trackIdx && frameDrag.destination.frameIdx === frameIdx}"
                                :title="`${track.property}, frame: ${frame.frame}, interpolation: ${frame.kind}, value: ${frame.value}`"
                                draggable="true"
                                @dblclick="onFrameDoubleClick(trackIdx, frameIdx)"
                                @dragstart="preventEvent"
                                @drag="preventEvent"
                                @mousedown="onMatrixFrameMouseDown(trackIdx, frameIdx, $event)"
                                @contextmenu="onFrameRightClick($event, trackIdx, frameIdx)"
                                >
                                <div class="active-frame" v-if="!frame.blank && !(frameDrag.on && trackIdx === frameDrag.source.trackIdx && frameIdx === frameDrag.source.frameIdx)">
                                    <i v-if="frame.icon === 'visibility-true'" class="fas fa-lightbulb"></i>
                                    <i v-else-if="frame.icon === 'visibility-false'" class="far fa-lightbulb"></i>
                                    <i v-else-if="frame.icon === 'opacity-none'" class="fa-regular fa-circle"></i>
                                    <i v-else-if="frame.icon === 'opacity-low'" class="fa-solid fa-circle-half-stroke"></i>
                                    <i v-else class="fas fa-circle"></i>
                                </div>
                                <div v-else class="empty-frame-number">{{frame.frame}}</div>
                            </td>
                        </tr>
                        <tr>
                            <td :colspan="totalFrames + 1" class="frame-animator-buttons">
                                <span class="btn btn-small" @click="toggleFunctionAddModal()">Add path function</span>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
        <div ref="frameDragPreview" class="frame-drag-preview" :class="{'is-dragging': frameDrag.on}">
            <span class="active-frame" v-if="!frameDrag.source.blank"><i class="fas fa-circle"></i></span>
        </div>

        <div ref="trackDragPreview" class="track-drag-preview" :class="{'is-dragging': trackDrag.on}">
            {{trackDrag.text}}
        </div>

        <ContextMenu v-if="frameContextMenu.shown"
            :mouseX="frameContextMenu.mouseX"
            :mouseY="frameContextMenu.mouseY"
            :options="frameContextMenu.options"
            @selected="onContextMenuOptionClick"
            @close="frameContextMenu.shown = false"
            />

        <modal v-if="functionEditorModal.shown && functionEditorModal.functionDescription"
            :title="`&quot;${functionEditorModal.functionDescription.name}&quot; arguments`"
            :primaryButton="functionEditorModal.isAdding ? 'Add' : null"
            :closeName="functionEditorModal.isAdding ? 'Cancel' : 'Close'"
            @close="functionEditorModal.shown = false"
            :width="400"
            :use-mask="false"
            @primary-submit="onFunctionModalSubmit()"
            >
            <div style="max-width:400px">
                <ArgumentsEditor
                    :editorId="editorId"
                    :argsDefinition="functionEditorModal.functionDescription.args"
                    :args="functionEditorModal.args"
                    :schemeContainer="schemeContainer"
                    @argument-changed="onFunctionModalArgumentChanged"
                    />
            </div>
        </modal>

        <modal v-if="addTrackModal.shown"
            title="Add animation track"
            primaryButton="Add track"
            @close="addTrackModal.shown = false"
            @primary-submit="addTrackFromModal()"
            :width="400"
            :use-mask="false"
            >
            <p>Select the item and its property</p>
            <table class="properties-table">
                <tbody>
                    <tr>
                        <td class="label" width="150px">
                            Item
                        </td>
                        <td>
                            <ElementPicker
                                :editorId="editorId"
                                :scheme-container="schemeContainer"
                                :element="addTrackModal.element"
                                :useSelf="false"
                                :allowTags="false"
                                :allowNone="false"
                                @selected="onAddTrackModalElementSelected(arguments[0])"
                            />
                        </td>
                    </tr>
                    <tr>
                        <td class="label" :class="{disabled: !addTrackModal.element}" width="150px">
                            Property
                        </td>
                        <td>
                            <Dropdown
                                :key="`track-property-dropdown-${editorId}-${addTrackModal.element}`"
                                :disabled="!addTrackModal.element"
                                :options="addTrackModal.properties"
                                @selected="onAddTrackModalPropertySelected"
                                >
                                <span v-if="addTrackModal.selectedPropertyOption">
                                    <i :class="addTrackModal.selectedPropertyOption.iconClass"></i>
                                    {{ addTrackModal.selectedPropertyOption.name }}
                                </span>
                                <span v-else>Choose property</span>
                            </Dropdown>
                        </td>
                    </tr>
                </tbody>
            </table>
        </modal>
    </div>
</template>

<script>
import SchemeContainer from '../../../scheme/SchemeContainer';
import ContextMenu from '../ContextMenu.vue';
import utils from '../../../utils';
import {dragAndDropBuilder} from '../../../dragndrop';
import {forEach, find, forEachObject} from '../../../collections';
import { jsonDiff } from '../../../json-differ';
import { compileAnimations, findItemPropertyDescriptor, findSchemePropertyDescriptor, interpolateValue } from '../../../animations/FrameAnimation';
import { Interpolations } from '../../../animations/ValueAnimation';
import PropertyInput from '../properties/PropertyInput.vue';
import Modal from '../../Modal.vue';
import AnimationFunctions from '../../../animations/functions/AnimationFunctions';
import ArgumentsEditor from '../ArgumentsEditor.vue';
import shortid from 'shortid';
import myMath from '../../../myMath';
import EditorEventBus from '../EditorEventBus';
import ElementPicker from '../ElementPicker.vue';
import Dropdown from '../../Dropdown.vue';
import Shape from '../items/shapes/Shape';

const validItemFieldPaths = new Set(['area', 'opacity', 'selfOpacity', 'visible', 'shapeProps']);



function calculateTrackColor(kind, id, property) {
    let hue = Math.abs(utils.hashString(kind + id)) % 360;
    const propertyHash = Math.abs(utils.hashString(property));
    const hueJitter = propertyHash % 20 - 10;
    hue = (hue + hueJitter) % 360;

    const saturationRange = [20, 70];
    const lightnessRange = [87, 97];

    const s = propertyHash % (saturationRange[1] - saturationRange[0]) + saturationRange[0];
    const l = propertyHash % (lightnessRange[1] - lightnessRange[0]) + lightnessRange[0];

    return `hsl(${hue}, ${s}%, ${l}%)`;
}


const connectorExcludedShapeProps = ['points', 'paths', 'sourceItem', 'destinationItem', 'sourceItemPosition', 'destinationItemPosition'];

function jsonDiffItemWhitelistCallback(item) {
    return (path) => {
        if (!validItemFieldPaths.has(path[0])) {
            return false;
        }

        if ((item.shape === 'connector' || item.shape === 'path') && path.length > 1 && path[0] === 'shapeProps') {
            if (connectorExcludedShapeProps.indexOf(path[1]) >= 0) {
                return false;
            }
        }

        if (item.shape === 'frame_player' && path.length > 1 && path[0] === 'shapeProps') {
            return false;
        }
        return true;
    };
}

function createTrackPropertiesOptionsForShape(shapeId) {
    const options = [{
        name: 'Position X',
        id: 'area.x',
        iconClass: 'fa-solid fa-arrows-left-right'
    }, {
        name: 'Position Y',
        id: 'area.y',
        iconClass: 'fa-solid fa-arrows-up-down'
    }, {
        name: 'Rotation',
        id: 'area.r',
        iconClass: 'fa-solid fa-arrows-rotate'
    }, {
        name: 'Scale X',
        id: 'area.sx',
        iconClass: 'fa-solid fa-down-left-and-up-right-to-center'
    }, {
        name: 'Scale Y',
        id: 'area.sy',
        iconClass: 'fa-solid fa-down-left-and-up-right-to-center'
    }, {
        name: 'Opacity',
        id: 'opacity',
        iconClass: 'fa-solid fa-lightbulb'
    }, {
        name: 'Self Opacity',
        id: 'selfOpacity',
        iconClass: 'fa-solid fa-lightbulb'
    }, {
        name: 'Visible',
        id: 'selfOpacity',
        iconClass: 'fa-regular fa-lightbulb'
    }, {
        name: 'Blend Mode',
        id: 'blendMode',
        iconClass: 'fa-solid fa-gear'
    }];

    const shape = Shape.find(shapeId);
    if (!shape) {
        return options;
    }

    forEachObject(Shape.getShapeArgs(shape), (arg, argName) => {
        if (shapeId === 'connector' || shapeId === 'path') {
            if (connectorExcludedShapeProps.indexOf(argName) >= 0) {
                return;
            }
        }

        options.push({
            name: arg.name,
            id: `shapeProps.${argName}`,
            iconClass: 'fa-solid fa-gear'
        });
    });
    return options;
}


function detectChanges(schemeContainer, originSchemeContainer) {
    const changes = [];

    forEach(originSchemeContainer.getItems(), originItem => {
        const item = schemeContainer.findItemById(originItem.id);
        if (!item) {
            //ignoring it
            return;
        }

        const diff = jsonDiff(originItem, item, {
            fieldCheck: jsonDiffItemWhitelistCallback(item),
        });
        if (diff.changes && diff.changes.length > 0) {
            forEach(diff.changes, change => {
                if (change.path.length === 0) {
                    return ;
                }

                changes.push({
                    kind: 'item',
                    id: item.id,
                    property: change.path.join('.'),
                    value: change.value,
                    oldValue: change.oldValue
                });
            });
        }
    });


    if (schemeContainer.scheme.style.backgroundColor !== originSchemeContainer.scheme.style.backgroundColor) {
        changes.push({
            kind: 'scheme',
            id: null,
            property: 'style.backgroundColor',
            value: schemeContainer.scheme.style.backgroundColor
        });
    }
    return changes;
}

const MIN_FPS = 0.01;

let _isPlayingAnimation = false;

function stopAnimations() {
    _isPlayingAnimation = false;
}

function playAnimations(schemeContainer, animations, startFrame, fps, maxFrames, {onFrame, onFinish}) {
    if (_isPlayingAnimation) {
        return;
    }

    _isPlayingAnimation = true;

    let totalTimePassed = 0;
    let currentFrame = startFrame;

    onFrame(currentFrame);

    const loopCycle = (timeMarker, dt) => {

        totalTimePassed += dt;
        let frame = startFrame + totalTimePassed * Math.max(fps, MIN_FPS) / 1000;
        let nextFrame = Math.floor(frame);

        if (nextFrame > currentFrame) {
            currentFrame = nextFrame;
            onFrame(currentFrame);
        }

        forEach(animations, animation => {
            animation.toggleFrame(frame);
        });
        schemeContainer.updateEditBox();


        if (nextFrame < maxFrames && _isPlayingAnimation) {
            window.requestAnimationFrame(() => {
                const nextTimeMarker = performance.now();
                loopCycle(nextTimeMarker, nextTimeMarker - timeMarker);
            });
        } else {
            _isPlayingAnimation = false;
            onFinish();
        }
    };

    loopCycle(performance.now(), 0);
}


function findFrameIdx(animation, frame) {
    for (let i = 0; i < animation.frames.length; i++) {
        if (animation.frames[i].frame === frame) {
            return i;
        }
    }
    return -1;
};



export default {
    props: {
        editorId         : {type: String, required: true},
        schemeContainer  : {type: Object, required: true},
        framePlayerItemId: {type: String, required: true},
        light            : {type: Boolean, default: true},
    },

    components: { ContextMenu, PropertyInput, ArgumentsEditor, Modal, ElementPicker, Dropdown },

    beforeMount() {
        this.compileAnimations();
        EditorEventBus.schemeChangeCommitted.$on(this.editorId, this.onSchemeChange);
        EditorEventBus.item.changed.specific.$on(this.editorId, this.framePlayerItemId, this.onFramePlayerChanged);
        EditorEventBus.schemeRebased.$on(this.editorId, this.onSchemeRebased);
    },

    beforeDestroy() {
        EditorEventBus.schemeChangeCommitted.$off(this.editorId, this.onSchemeChange);
        EditorEventBus.item.changed.specific.$off(this.editorId, this.framePlayerItemId, this.onFramePlayerChanged);
        EditorEventBus.schemeRebased.$off(this.editorId, this.onSchemeRebased);
        this.$emit('recording-state-updated', false);
    },

    data() {
        const framePlayer = this.schemeContainer.findItemById(this.framePlayerItemId);
        return {
            framePlayer: framePlayer,
            originSchemeContainer: null,
            currentFrame: 1,
            totalFrames: framePlayer.shapeProps.totalFrames,
            framesMatrix: this.buildFramesMatrix(framePlayer),
            compiledAnimations: [],
            isPlaying: false,
            isRecording: false,
            recordingPushbackTimer: null,
            recordingPushbackInterval: 100,

            selectedTrackIdx: -1,

            frameDrag: {
                on: false,
                source: {
                    blank: true,
                    trackIdx: -1,
                    frameIdx: -1
                },
                destination: {
                    frameIdx: -1
                }
            },

            trackDrag: {
                on: false,
                srcAnimationId: null,
                srcTrackIdx: -1,
                kind: null,
                text: 'Drag it',
                dstTrackIdx: -1,
                dropHead: false
            },

            frameContextMenu: {
                shown: false,
                mouseX: 0,
                mouseY: 0,
                options: []
            },

            selectedFrameControl: {
                value: null,
                trackIdx: -1,
                frame: -1,
                blank: true,
                propertyDescriptor: null
            },

            shouldRecompileAnimations: false,

            functionEditorModal: {
                shown: false,
                funcName: null,
                functionId: null,
                args: null,
                isAdding: true,
                functionDescription: null
            },

            addTrackModal: {
                element: null,
                property: null,
                selectedPropertyOption: null,
                properties: [],
                shown: false,
            }
        };
    },

    methods: {
        openAnimationEditor() {
            this.$emit('animation-editor-opened', this.framePlayer);
        },

        selectFrame(frame) {
            this.currentFrame = frame;
            if (this.shouldRecompileAnimations) {
                this.compileAnimations();
            }
            forEach(this.compiledAnimations, compiledAnimation => {
                compiledAnimation.toggleFrame(frame);
            });

            this.schemeContainer.updateEditBox();

            if (this.selectedFrameControl.trackIdx >= 0 ) {
                this.selectFrameControl(this.selectedFrameControl.trackIdx, frame - 1);
            }

            if (this.isRecording) {
                this.originSchemeContainer = new SchemeContainer(utils.clone(this.schemeContainer.scheme), this.editorId, 'edit', this.$store.state.apiClient);
            }
        },

        selectTrackAndFrame(trackIdx, frameIdx) {
            this.selectedTrackIdx = trackIdx;
            const track = this.framesMatrix[trackIdx];
            this.selectFrameControl(trackIdx, frameIdx);
            this.selectFrame(track.frames[frameIdx].frame)
        },

        selectFrameControl(trackIdx, frameIdx) {
            const track = this.framesMatrix[trackIdx];
            if (!track.frames) {
                return;
            }
            const frame = track.frames[frameIdx];

            this.selectedFrameControl.propertyDescriptor = track.propertyDescriptor;

            if (frame.blank) {
                this.selectedFrameControl.blank = true;
            } else {
                this.selectedFrameControl.blank = false;
                this.selectedFrameControl.value = frame.value;
            }

            this.selectedFrameControl.trackIdx = trackIdx;
            this.selectedFrameControl.frame = frame.frame;
        },

        resetFrameControl() {
            this.selectedFrameControl.trackIdx = -1;
            this.selectedFrameControl.frame = -1;
            this.selectedFrameControl.propertyDescriptor = null;
        },

        onFrameDoubleClick(trackIdx, frameIdx) {
            const track = this.framesMatrix[trackIdx];
            if (track.kind === 'sections') {
                this.addSectionFrame(trackIdx, frameIdx);
                this.selectTrackAndFrame(trackIdx, frameIdx);
            } else if (track.kind === 'function') {
                this.addFunctionFrame(trackIdx, frameIdx);
                this.selectTrackAndFrame(trackIdx, frameIdx);
            } else {
                const frame = track.frames[frameIdx];
                if (frame.blank) {
                    this.recordCurrentValueForTrack(track, frame);
                    this.selectTrackAndFrame(trackIdx, frameIdx);
                }
            }
        },

        fillMatrixFrames(framePlayer, originFrames, valueType) {
            const matrixFrames = [];

            originFrames.sort((a, b) => a.frame - b.frame);

            const addBlankFrames = (num) => {
                const length = matrixFrames.length;
                for (let i = 0; i < num; i++) {
                    matrixFrames.push({
                        frame: i + length + 1,
                        blank: true
                    });
                }
            };

            const totalFrames = framePlayer.shapeProps.totalFrames;

            let prevFrame = null;
            forEach(originFrames, f => {
                if (f.frame - 1 > matrixFrames.length && f.frame <= totalFrames) {
                    // should fill with empty cells first and then add the frame
                    addBlankFrames(f.frame - 1 - matrixFrames.length);
                }
                if (!prevFrame || prevFrame.frame !== f.frame) {
                    // protect from duplicate frames
                    if (f.frame <= totalFrames) {
                        const activeFrame = utils.clone(f);
                        this.enrichFrameWithIcon(activeFrame, valueType);
                        matrixFrames.push(activeFrame);
                    }
                }
                prevFrame = f;
            });
            if (matrixFrames.length < totalFrames) {
                addBlankFrames(totalFrames - matrixFrames.length);
            }
            return matrixFrames;
        },

        enrichFrameWithIcon(frame, valueType) {
            if (valueType === 'visibility') {
                frame.icon = 'visibility-' + frame.value;
            } else if (valueType === 'opacity') {
                if (frame.value <= 25) {
                    frame.icon = 'opacity-none';
                } else if (frame.value < 75) {
                    frame.icon = 'opacity-low';
                } else {
                    frame.icon = 'opacity-high';
                }
            }
        },

        buildFramesMatrix(framePlayer) {
            const matrix = [];

            this.fixAllAnimationFrames(framePlayer);

            forEach(framePlayer.shapeProps.animations, animation => {
                if (animation.kind === 'function') {
                    // skipping animation tracks as they are going to be built in another function
                    return;
                }

                let itemName = null;
                let propertyDescriptor = null;
                let valueType = null;

                if (animation.kind === 'item') {
                    const item = this.schemeContainer.findItemById(animation.itemId);
                    if (item) {
                        itemName = item.name;
                        propertyDescriptor = findItemPropertyDescriptor(item, animation.property);

                        if (animation.property === 'visible') {
                            valueType = 'visibility';
                        } else if (animation.property === 'opacity' || animation.property === 'selfOpacity') {
                            valueType = 'opacity';
                        } else if (propertyDescriptor) {
                            valueType = propertyDescriptor.type;
                        }
                    }
                } else if (animation.kind === 'scheme') {
                    propertyDescriptor = findSchemePropertyDescriptor(animation.property);
                }

                const frames = this.fillMatrixFrames(framePlayer, animation.frames, valueType);

                const track = {
                    kind    : animation.kind,
                    id      : animation.id,
                    itemId  : animation.itemId,
                    property: animation.property,
                    color   : calculateTrackColor(animation.kind, animation.itemId, animation.property),
                    propertyDescriptor,
                    valueType,
                    itemName,
                    frames,
                };
                matrix.push(track);
            });

            matrix.push(this.buildSectionsTrack(framePlayer));
            if (framePlayer.shapeProps.animations.length > 5) {
                matrix.push({
                    kind: 'panel-buttons'
                });
            }
            return matrix.concat(this.buildFunctionTracks(framePlayer));
        },

        // sorts frames, removes duplicate frames
        fixAllAnimationFrames(framePlayer) {
            const sortAndDeduplicate = (frames) => {
                frames.sort((a, b) => a.frame - b.frame);
                if (frames.length < 2) {
                    return;
                }
                for (let i = 1; i < frames.length; i++) {
                    if (frames[i].frame === frames[i - 1].frame) {
                        frames.splice(i, 1);
                        i = i - 1;
                    }
                }
            }

            forEach(framePlayer.shapeProps.animations, animation => {
                sortAndDeduplicate(animation.frames);
            });
            sortAndDeduplicate(framePlayer.shapeProps.sections);
        },

        buildSectionsTrack(framePlayer) {
            let sections = framePlayer.shapeProps.sections;
            if (!sections) {
                sections = [];
            }
            const frames = this.fillMatrixFrames(framePlayer, sections, 'section');

            return {
                kind: 'sections',
                propertyDescriptor: {type: 'string', name: 'Section Name'},
                frames
            };
        },

        buildFunctionTracks(framePlayer) {
            const tracks = [];
            forEach(framePlayer.shapeProps.animations, animation => {
                if (animation.kind !== 'function') {
                    return;
                }

                const func = framePlayer.shapeProps.functions[animation.funcId];
                if (!func) {
                    return;
                }

                const functionDescription = AnimationFunctions[func.functionId];
                if (!functionDescription) {
                    return;
                }

                tracks.push({
                    kind  : 'function-header',
                    id    : animation.id,
                    funcId: animation.funcId,
                    name  : functionDescription.getFullName(func.args, this.schemeContainer),
                });

                const propertyDescriptor = functionDescription.inputs[animation.property];
                if (!propertyDescriptor) {
                    return;
                }
                tracks.push({
                    kind    : 'function',
                    id      : animation.id,
                    funcId  : animation.funcId,
                    property: animation.property,
                    frames  : this.fillMatrixFrames(framePlayer, animation.frames, 'number'),
                    propertyDescriptor,
                });
            });
            return tracks;
        },

        onSchemeRebased() {
            const item = this.schemeContainer.findItemById(this.framePlayer.id);
            if (item) {
                this.framePlayer = item;
            }
            this.compileAnimations();
            this.updateFramesMatrix();
        },

        startRecording() {
            this.isRecording = true;
            this.$emit('recording-state-updated', true);
            this.originSchemeContainer = new SchemeContainer(utils.clone(this.schemeContainer.scheme), this.editorId, 'edit', this.$store.state.apiClient);
        },

        stopRecording() {
            this.isRecording = false;
            this.$emit('recording-state-updated', false);
        },

        onSchemeChange() {
            if (!this.isRecording) {
                return;
            }

            if (this.recordingPushbackTimer) {
                clearTimeout(this.recordingPushbackTimer);
            }

            this.recordingPushbackTimer = setTimeout(() => {
                this.recordCurrentFrame();
                this.recordingPushbackTimer = null;
            }, this.recordingPushbackInterval);
        },

        recordCurrentFrame() {
            const changes = detectChanges(this.schemeContainer, this.originSchemeContainer);

            const changeMatchesAnimation = (change, animation) => {
                if (change.kind !== animation.kind || change.property !== animation.property) {
                    return false;
                }
                if (change.kind === 'item') {
                    return change.id === animation.itemId;
                }
                if (change.kind === 'scheme') {
                    return true;
                }
                return false;
            };
            forEach(changes, change => {

                // checking if the change was just too small
                // this is needed in situations when item was rotated
                // due to how item edit box is implemented it has to readjust items position
                // and often there is a small error in its calculation
                if (typeof change.value === 'number' && typeof change.oldValue === 'number') {
                    if (myMath.tooSmall(change.value - change.oldValue)) {
                        return;
                    }
                }

                let animation = find(this.framePlayer.shapeProps.animations, animation => changeMatchesAnimation(change, animation));
                if (!animation) {
                    const frames = [{
                        frame: this.currentFrame,
                        value: change.value,
                        kind : 'linear'
                    }];
                    if (this.currentFrame > 1) {
                        frames.splice(0, 0, {
                            frame: 1,
                            value: change.oldValue,
                            kind : 'linear'
                        });
                    }
                    const track = {
                        kind    : change.kind,
                        id      : shortid.generate(),
                        property: change.property,
                        frames  : frames
                    };
                    if (change.kind === 'item') {
                        track.itemId = change.id;
                    }
                    this.framePlayer.shapeProps.animations.push(track);
                } else {
                    let idx = -1;
                    let found = false;
                    let foundFrame = null;
                    for (let i = 0; i < animation.frames.length && !found; i++) {
                        const f = animation.frames[i];
                        if (f.frame === this.currentFrame) {
                            idx = i;
                            found = true;
                            foundFrame = f;
                            this.shouldRecompileAnimations = true;
                        } else {
                            found = true;
                        }
                    }

                    if (foundFrame && idx >=0) {
                        if (foundFrame.frame === this.currentFrame) {
                            foundFrame.value = change.value;
                        } else {
                            animation.frames.splice(idx, 0, {
                                frame: this.currentFrame,
                                value: change.value,
                                kind : 'linear'
                            });
                        }
                    } else {
                        animation.frames.splice(0, 0, {
                            frame: this.currentFrame,
                            value: change.value,
                            kind : 'linear'
                        });
                    }
                }
            });
            this.updateFramesMatrix();
        },

        updateFramesMatrix() {
            this.framesMatrix = this.buildFramesMatrix(this.framePlayer);
            this.shouldRecompileAnimations = true;
        },

        compileAnimations() {
            this.compiledAnimations = compileAnimations(this.framePlayer, this.schemeContainer);
            this.shouldRecompileAnimations = false;
        },

        removeAnimationTrack(track) {
            if (track.kind === 'function-header') {
                // should remove function and all its assosiated property tracks
                if (this.framePlayer.shapeProps.functions.hasOwnProperty(track.funcId)) {
                    delete this.framePlayer.shapeProps.functions[track.funcId];
                }

                const animations = this.framePlayer.shapeProps.animations;
                for (let i = 0; i < animations.length; i++) {
                    if (animations[i].kind === 'function' && animations[i].id === track.id) {
                        animations.splice(i, 1);
                        i = i - 1;
                    }
                }
                this.updateFramesMatrix();
            } else if (track.kind === 'item') {
                const idx = this.findAnimationIndexForTrack(track);
                if (idx < 0) {
                    return null;
                }
                this.framePlayer.shapeProps.animations.splice(idx, 1);
                this.updateFramesMatrix();
            }
        },

        findAnimationIndexForTrack(track) {
            for (let i = 0; i < this.framePlayer.shapeProps.animations.length; i++) {
                const animation = this.framePlayer.shapeProps.animations[i];
                if (animation.kind === track.kind && animation.id === track.id && animation.property === track.property) {
                    return i;
                }
            }
            return -1;
        },

        preventEvent(event) {
            event.preventDefault();
            event.stopPropagation();
        },

        onMatrixFrameMouseDown(trackIdx, frameIdx, event) {
            const sourceTrack = this.framesMatrix[trackIdx];
            const sourceFrame = sourceTrack.frames[frameIdx];

            dragAndDropBuilder(event)
            .withDraggedElement(this.$refs.frameDragPreview)
            .withDroppableClass('frame-droppable-area')
            .onSimpleClick(() => {
                this.selectTrackAndFrame(trackIdx, frameIdx);
            })
            .onDragStart(() => {
                this.frameDrag.on = true;
                this.frameDrag.source.blank = sourceFrame.blank;
                this.frameDrag.source.trackIdx = trackIdx;
                this.frameDrag.source.frameIdx = frameIdx;
            })
            .onDragOver((event, frameElement) => {
                const droppableTrackIdx = parseInt(frameElement.getAttribute('data-track-idx'));
                const droppableFrameIdx = parseInt(frameElement.getAttribute('data-frame-idx'));
                if (droppableTrackIdx === this.frameDrag.source.trackIdx) {
                    this.frameDrag.destination.frameIdx = droppableFrameIdx;
                } else {
                    this.frameDrag.destination.frameIdx = -1;
                }
            })
            .onDrop((event, frameElement) => {
                const droppableTrackIdx = parseInt(frameElement.getAttribute('data-track-idx'));
                const droppableFrameIdx = parseInt(frameElement.getAttribute('data-frame-idx'));
                if (droppableTrackIdx === this.frameDrag.source.trackIdx) {
                    this.onMatrixDragEnd(this.frameDrag.source.trackIdx, this.frameDrag.source.frameIdx, droppableFrameIdx);

                }
            })
            .onDone((event) => {
                this.frameDrag.on = false;
                this.frameDrag.source.trackIdx = -1;
                this.frameDrag.source.frameIdx = -1;
                this.frameDrag.destination.frameIdx = -1;
            })
            .build();
        },

        onMatrixDragEnd(srcTrackIdx, srcFrameIdx, dstFrameIdx) {
            if (dstFrameIdx === srcFrameIdx || dstFrameIdx < 0) {
                return;
            }
            const track = this.framesMatrix[srcTrackIdx];

            const sourceFrame = track.frames[srcFrameIdx];
            const destinationFrame = track.frames[dstFrameIdx];

            if (track.kind === 'sections') {
                const sections = this.framePlayer.shapeProps.sections;
                if (sourceFrame.blank) {
                    // deleting
                    let matchingIdx = -1;
                    for (let i = 0; i < sections.length && matchingIdx < 0; i++) {
                        if (sections[i].frame === destinationFrame.frame) {
                            matchingIdx = i;
                        }
                    }
                    if (matchingIdx >= 0) {
                        sections.splice(matchingIdx, 1);
                    }
                } else {
                    // replacing or inserting
                    const frame = {
                        frame: destinationFrame.frame,
                        value: sourceFrame.value
                    }
                    let matchingIdx = -1;
                    for (let i = 0; i < sections.length && matchingIdx < 0; i++) {
                        if (sections[i].frame === sourceFrame.frame) {
                            matchingIdx = i;
                        }
                    }
                    if (matchingIdx >= 0) {
                        sections[matchingIdx] = frame;
                    } else {
                        sections.push(frame);
                    }
                    sections.sort((a, b) => a.frame - b.frame);
                }
            } else {
                //TODO simplify all this code and implement in the same way as we handle it for sections
                const animationIdx = this.findAnimationIndexForTrack(track);
                if (animationIdx < 0) {
                    return;
                }

                const animation = this.framePlayer.shapeProps.animations[animationIdx];

                if (sourceFrame.blank && !destinationFrame.blank) {
                    // deleting frame
                    const frameIdx = findFrameIdx(animation, destinationFrame.frame);
                    if (frameIdx < 0) {
                        return;
                    }
                    animation.frames.splice(frameIdx, 1);
                } else if (!sourceFrame.blank) {
                    if (destinationFrame.blank) {
                        // changing the frame number
                        const frameIdx = findFrameIdx(animation, sourceFrame.frame);
                        if (frameIdx < 0) {
                            return;
                        }
                        animation.frames[frameIdx].frame = dstFrameIdx + 1;
                    } else {
                        // deleting destination frame and changing the frame number on the source
                        const srcOriginFrameIdx = findFrameIdx(animation, sourceFrame.frame);
                        if (srcOriginFrameIdx < 0) {
                            return;
                        }
                        const dstOriginFrameIdx = findFrameIdx(animation, destinationFrame.frame);
                        if (dstOriginFrameIdx < 0) {
                            return;
                        }
                        animation.frames[srcOriginFrameIdx].frame = dstFrameIdx + 1;
                        animation.frames.splice(dstOriginFrameIdx, 1);
                    }
                    animation.frames.sort((a, b) => a.frame - b.frame);
                }

            }

            this.updateFramesMatrix();
        },

        playAnimations() {
            this.resetFrameControl();
            if (this.shouldRecompileAnimations) {
                this.compileAnimations();
            }
            this.isPlaying = true;
            playAnimations(this.schemeContainer, this.compiledAnimations, this.currentFrame, this.framePlayer.shapeProps.fps, this.framePlayer.shapeProps.totalFrames, {
                onFrame: (frame) => {
                    this.currentFrame = frame;
                },
                onFinish: () => {
                    this.isPlaying = false;
                }
            });
        },

        stopAnimations() {
            stopAnimations();
        },

        moveFrameLeft() {
            if (!this.isPlaying) {
                if (this.currentFrame > 1) {
                    this.selectFrame(this.currentFrame - 1);
                }
            }
        },

        moveFrameRight() {
            if (!this.isPlaying) {
                if (this.currentFrame < this.framePlayer.shapeProps.totalFrames) {
                    this.selectFrame(this.currentFrame + 1);
                }
            }
        },

        onFrameRightClick(event, trackIdx, frameIdx) {
            event.preventDefault();
            const track = this.framesMatrix[trackIdx];
            const frame = track.frames[frameIdx];
            const options = [{
                name: 'Insert empty frames',
                iconClass: 'fa-solid fa-plus',
                clicked: () => {
                    this.insertEmptyFramesAtFrame(frameIdx + 1);
                }
            }];

            if (this.framePlayer.shapeProps.totalFrames > 1) {
                options.push({
                    name: 'Delete frame for all tracks',
                    iconClass: 'fas fa-trash',
                    clicked: () => {
                        this.deleteFrames(frameIdx + 1);
                    }
                });
            }

            if (track.kind !== 'function') {
                options.push({
                    name: 'Record current value',
                    iconClass: 'far fa-dot-circle',
                    clicked: () => {
                        this.recordCurrentValueForTrack(track, frame);
                    }
                });
            }
            if (!frame.blank) {
                options.push({
                    name: 'Clear frame',
                    iconClass: 'fa-solid fa-broom',
                    clicked: () => {
                        if (track.kind === 'sections') {
                            const sections = this.framePlayer.shapeProps.sections;
                            for (let i = 0; i < sections.length; i++) {
                                if (sections[i].frame === frame.frame) {
                                    sections.splice(i, 1);
                                    track.frames[frameIdx] = {
                                        frame: frame.frame,
                                        blank: true
                                    };
                                    break;
                                }
                            }
                        } else {
                            const animationIdx = this.findAnimationIndexForTrack(track);
                            if (animationIdx < 0) {
                                return;
                            }
                            const frameIdx = findFrameIdx(this.framePlayer.shapeProps.animations[animationIdx], frame.frame);
                            if (frameIdx < 0) {
                                return;
                            }

                            this.framePlayer.shapeProps.animations[animationIdx].frames.splice(frameIdx, 1);
                            this.updateFramesMatrix();
                        }
                        if (this.selectedTrackIdx === trackIdx && this.selectedFrameControl.frame === frame.frame) {
                            this.selectTrackAndFrame(trackIdx, frameIdx);
                        }
                    }
                });

                forEach(Interpolations.values(), interpolation => {
                    if (frame.kind !== interpolation) {
                        options.push({
                            name: 'Convert to ' + interpolation,
                            interpolation,
                            clicked: (option) => {
                                const animationIdx = this.findAnimationIndexForTrack(track);
                                if (animationIdx < 0) {
                                    return;
                                }
                                const frameIdx = findFrameIdx(this.framePlayer.shapeProps.animations[animationIdx], frame.frame);
                                if (frameIdx < 0) {
                                    return;
                                }

                                this.framePlayer.shapeProps.animations[animationIdx].frames[frameIdx].kind = option.interpolation
                                this.updateFramesMatrix();
                            }
                        });
                    }
                });
            }

            if (options.length === 0) {
                return;
            }

            this.frameContextMenu.options = options;
            this.frameContextMenu.mouseX = event.pageX;
            this.frameContextMenu.mouseY = event.pageY;
            this.frameContextMenu.shown = true;
        },

        deleteFrames(frameNumber) {
            const deleteFrame = (frames) => {
                for (let i = frames.length - 1; i >= 0; i -= 1) {
                    if (frames[i].frame > frameNumber) {
                        frames[i].frame -= 1;
                    } else if (frames[i].frame === frameNumber) {
                        frames.splice(i, 1);
                    }
                }
            };

            deleteFrame(this.framePlayer.shapeProps.sections);
            this.framePlayer.shapeProps.animations.forEach(animation => {
                deleteFrame(animation.frames);
            });

            this.framePlayer.shapeProps.totalFrames -= 1;
            this.totalFrames = this.framePlayer.shapeProps.totalFrames;
            this.updateFramesMatrix();
            EditorEventBus.schemeChangeCommitted.$emit(this.editorId);
        },

        insertEmptyFramesAtFrame(frameNumber) {
            // this code simulates insertion of empty frames.
            // since we don't really store empty frames in shapeProps
            // we just need to increase frames that are >= than given frame number
            const insertEmptyFrame = (frames) => {
                for (let i = 0; i < frames.length; i++) {
                    if (frames[i].frame >= frameNumber) {
                        frames[i].frame += 1;
                    }
                }
            };

            insertEmptyFrame(this.framePlayer.shapeProps.sections);
            this.framePlayer.shapeProps.animations.forEach(animation => {
                insertEmptyFrame(animation.frames);
            });

            this.framePlayer.shapeProps.totalFrames += 1;
            this.totalFrames = this.framePlayer.shapeProps.totalFrames;
            this.updateFramesMatrix();
            EditorEventBus.schemeChangeCommitted.$emit(this.editorId);
        },

        onContextMenuOptionClick(option) {
            if (option.clicked) {
                option.clicked(option);
            }
        },

        recordCurrentValueForTrack(track, frame) {
            const animationIdx = this.findAnimationIndexForTrack(track);
            if (animationIdx < 0) {
                return;
            }
            const animation = this.framePlayer.shapeProps.animations[animationIdx];

            let value = 0;
            if (track.kind === 'item') {
                const item = this.schemeContainer.findItemById(track.itemId);
                if (!item) {
                    return;
                }
                value = utils.getObjectProperty(item, track.property);
            } else if (track.kind === 'scheme' ) {
                value = utils.getObjectProperty(this.schemeContainer.scheme, track.property);
            } else {
                // anything else is not supported yet
                return;
            }

            const frameIdx = findFrameIdx(animation, frame.frame);
            if (frameIdx >= 0) {
                animation.frames[frameIdx].value = value;
            } else {
                // searching for frame that is bigger than current
                let idx = 0;
                let interpolation = 'linear';

                for (let i = 0; i < animation.frames.length; i++) {
                    interpolation = animation.frames[i].kind;
                    if (animation.frames[i].frame > frame.frame) {
                        idx = i;
                        break;
                    }
                }

                animation.frames.splice(idx, 0, {
                    frame: frame.frame,
                    kind : interpolation,
                    value: value
                });
            }

            this.updateFramesMatrix();
        },

        onFramePropertyInput(value) {
            const trackIdx = this.selectedFrameControl.trackIdx;
            const frame = this.selectedFrameControl.frame;
            if (trackIdx < 0 || frame < 0) {
                return;
            }
            const track = this.framesMatrix[trackIdx];

            this.selectedFrameControl.value = value;
            if (track.kind === 'item' || track.kind === 'scheme' || track.kind === 'function') {
                const animationIdx = this.findAnimationIndexForTrack(track);
                if (animationIdx < 0) {
                    return;
                }
                const animation = this.framePlayer.shapeProps.animations[animationIdx];
                const frameIdx = findFrameIdx(animation, frame);
                if (frameIdx < 0) {
                    return;
                }

                this.framePlayer.shapeProps.animations[animationIdx].frames[frameIdx].value = value;
                this.framesMatrix[trackIdx].frames[frame - 1].value = value;

                if (animation.kind === 'item') {
                    const item = this.schemeContainer.findItemById(animation.itemId);
                    if (item) {
                        utils.setObjectProperty(item, animation.property, value);
                        EditorEventBus.item.changed.specific.$emit(this.editorId, item.id, animation.property);

                        if (animation.property.startsWith('area.')) {
                            this.schemeContainer.updateEditBox();
                        }
                    }
                } else if (animation.kind === 'scheme') {
                    utils.setObjectProperty(this.schemeContainer.scheme, animation.property, value);
                } else if (animation.kind === 'function') {
                    this.selectFrame(frame);
                }

                if (track.valueType) {
                    this.enrichFrameWithIcon(this.framesMatrix[trackIdx].frames[frame - 1], track.valueType);
                }

                EditorEventBus.schemeChangeCommitted.$emit(this.editorId, `animation.${this.framePlayer.id}.track.${trackIdx}.frames.${frameIdx}.${animation.property}`);
                this.shouldRecompileAnimations = true;
            } else if (track.kind === 'sections') {
                const sections = this.framePlayer.shapeProps.sections;
                for (let i = 0; i < sections.length; i++) {
                    if (sections[i].frame === frame) {
                        sections[i].value = value;
                        track.frames[frame - 1].value = value;
                        break;
                    }
                }
                EditorEventBus.schemeChangeCommitted.$emit(this.editorId, `animation.${this.framePlayer.id}.sections.${frame}`);
            }
        },

        addSectionFrame(trackIdx, frameIdx) {
            const frameNum = frameIdx + 1;
            const frame = {
                frame: frameNum,
                value: `${frameNum}`,
                kind: 'step',
            };

            let idx = 0;
            const sections = this.framePlayer.shapeProps.sections;
            for (let i = 0; i < sections.length; i++) {
                if (sections[i].frame === frameNum) {
                    // such frame already exists so avoiding duplicates
                    return;
                }
                if (sections[i].frame > frameNum) {
                    idx = i;
                    break;
                }
            }

            this.framesMatrix[trackIdx].frames[frameIdx] = frame;
            sections.splice(idx, 0, frame);
            EditorEventBus.schemeChangeCommitted.$emit(this.editorId, `animation.${this.framePlayer.id}.sections.${frame}`);
        },

        addFunctionFrame(trackIdx, frameIdx) {
            const track = this.framesMatrix[trackIdx];
            const funcDef = this.framePlayer.shapeProps.functions[track.id];
            if (!funcDef) {
                return;
            }
            const animationIdx = this.findAnimationIndexForTrack(track);
            if (animationIdx < 0) {
                return;
            }

            const animation = this.framePlayer.shapeProps.animations[animationIdx];

            let idx = 0;
            let prevFrame = null;
            let nextFrame = null;
            const frameNum = frameIdx + 1;

            for (let i = 0; i < animation.frames.length; i++) {
                if (animation.frames[i].frame === frameNum) {
                    // such frame already exists so avoiding duplicates
                    return;
                }
                else if (animation.frames[i].frame > frameNum) {
                    idx = i;
                    nextFrame = animation.frames[i];
                    break;
                } else {
                    prevFrame = animation.frames[i];
                }
            }

            const frame = {
                frame: frameNum,
                kind: 'linear',
                value: 0
            };

            const functionDescriptor = AnimationFunctions[funcDef.functionId];
            if (!functionDescriptor) {
                return;
            }

            const inputDescriptor = functionDescriptor.inputs[animation.property];
            if (!inputDescriptor) {
                return;
            }

            if (prevFrame && nextFrame) {
                const t = (frameNum - prevFrame.frame ) / Math.max(1, nextFrame.frame - prevFrame.frame);
                frame.value = interpolateValue(inputDescriptor.type, prevFrame.value, nextFrame.value, t);
            } else if (prevFrame) {
                frame.value = prevFrame.value;
            } else if (nextFrame) {
                frame.value = prevFrame.value;
            } else {
                frame.value = inputDescriptor.value;
            }
            animation.frames.splice(idx, 0, frame);
            this.updateFramesMatrix();
        },

        toggleAddTrackModal() {
            this.addTrackModal.element = null;
            this.addTrackModal.property = null;
            this.addTrackModal.properties = [];
            this.addTrackModal.selectedPropertyOption = null;
            this.addTrackModal.shown = true;
        },

        addTrackFromModal() {
            const item = this.schemeContainer.findFirstElementBySelector(this.addTrackModal.element);
            if (item && this.addTrackModal.property) {
                const trackAlreadyExists = this.framePlayer.shapeProps.animations.findIndex(track =>
                    track.kind === 'item' && track.itemId === item.id && track.property === this.addTrackModal.property
                ) >= 0;

                if (!trackAlreadyExists) {
                    const track = {
                        kind    : 'item',
                        id      : shortid.generate(),
                        itemId  : item.id,
                        property: this.addTrackModal.property,
                        frames  : []
                    };
                    this.framePlayer.shapeProps.animations.push(track);
                    this.updateFramesMatrix();
                }
            }
            this.addTrackModal.shown = false;
        },

        onAddTrackModalElementSelected(element) {
            this.addTrackModal.element = element;
            const item = this.schemeContainer.findFirstElementBySelector(element);
            if (!item) {
                return;
            }

            this.addTrackModal.properties = createTrackPropertiesOptionsForShape(item.shape);
        },

        onAddTrackModalPropertySelected(option) {
            this.addTrackModal.property = option.id;
            this.addTrackModal.selectedPropertyOption = option;
        },

        toggleFunctionAddModal() {
            this.functionEditorModal.funcName = 'moveAlongPath';
            this.functionEditorModal.functionId = null;
            const func = AnimationFunctions[this.functionEditorModal.funcName];
            this.functionEditorModal.functionDescription = func;

            const args = {};
            forEach(func, (argDef, argName) => {
                args[argName] = argDef.value;
            });
            this.functionEditorModal.args = args;
            this.functionEditorModal.isAdding = true;
            this.functionEditorModal.shown = true;
        },

        toggleEditFunctionArgumentsForTrack(track) {
            const func = this.framePlayer.shapeProps.functions[track.funcId];
            if (!func) {
                return null;
            }

            this.functionEditorModal.funcName = track.functionId;
            this.functionEditorModal.functionDescription = AnimationFunctions[func.functionId];
            this.functionEditorModal.functionId = track.funcId;
            this.functionEditorModal.args = func.args;
            this.functionEditorModal.isAdding = false;

            if (this.functionEditorModal.functionDescription) {
                this.functionEditorModal.shown = true;
            }
        },

        onFunctionModalArgumentChanged(name, value) {
            this.functionEditorModal.args[name] = value;
            if (this.functionEditorModal.functionId) {
                EditorEventBus.schemeChangeCommitted.$emit(this.editorId);
                this.framePlayer.shapeProps.functions[this.functionEditorModal.functionId].args[name] = value;
            }
            this.compileAnimations();
        },

        onFunctionModalSubmit() {
            this.functionEditorModal.shown = false;
            if (this.functionEditorModal.isAdding) {
                const funcId = shortid.generate();
                const func = AnimationFunctions[this.functionEditorModal.funcName];
                if (!func) {
                    return;
                }

                this.framePlayer.shapeProps.functions[funcId] = {
                    functionId: this.functionEditorModal.funcName,
                    args: utils.clone(this.functionEditorModal.args)
                };

                forEach (func.inputs, (input, inputName) => {
                    this.framePlayer.shapeProps.animations.push({
                        kind: 'function',
                        id: shortid.generate(),
                        funcId: funcId,
                        property: inputName,
                        frames: [{
                            frame: 1,
                            kind: 'linear',
                            value: input.value
                        }, {
                            frame: this.framePlayer.shapeProps.totalFrames,
                            kind: 'linear',
                            value: input.endValue
                        }]
                    });
                });
                EditorEventBus.schemeChangeCommitted.$emit(this.editorId);
            }

            this.updateFramesMatrix();
        },

        onHistoryUndone() {
            this.updateFramesMatrix();
        },

        onFramePlayerChanged() {
            if (this.totalFrames !== this.framePlayer.shapeProps.totalFrames) {
                this.totalFrames = this.framePlayer.shapeProps.totalFrames;
                this.updateFramesMatrix();
            }
        },

        onTrackLabelMouseDown(trackIdx, originalEvent) {
            const track = this.framesMatrix[trackIdx];
            if (track.kind === 'sections' || track.kind === 'function') {
                return;
            };

            const handleDragOverAndDrop = (event, trackElement) => {
                const dstTrackIdx = parseInt(trackElement.getAttribute('data-track-index'));
                const dstTrack = this.framesMatrix[dstTrackIdx];

                if (this.trackDrag.kind === 'item') {
                    if (dstTrack.kind !== this.trackDrag.kind) {
                        this.trackDrag.dstTrackIdx = -1;
                        return;
                    }

                    const rect = trackElement.getBoundingClientRect();
                    const dropAbove = event.pageY < rect.top + rect.height / 2;
                    this.trackDrag.dstTrackIdx = dstTrackIdx;
                    this.trackDrag.dropHead = false;

                    if (dropAbove) {
                        if (dstTrackIdx > 0) {
                            this.trackDrag.dstTrackIdx = dstTrackIdx - 1;
                        } else {
                            this.trackDrag.dropHead = true;
                        }
                    }
                } else if (this.trackDrag.kind === 'function-header') {
                    this.trackDrag.dropHead = false;
                    if (dstTrack.kind === 'function-header' && dstTrack.id !== track.id) {
                        this.trackDrag.dstTrackIdx = dstTrackIdx - 1;

                    } else if (dstTrack.kind === 'function' && dstTrack.id !== track.id) {
                        let found = false;
                        let i = dstTrackIdx + 1;
                        for (; i < this.framesMatrix.length && !found; i++) {
                            if (this.framesMatrix[i].kind === 'function-header') {
                                found = true;
                            }
                        }
                        if (found) {
                            this.trackDrag.dstTrackIdx = i - 2;
                        } else {
                            this.trackDrag.dstTrackIdx = this.framesMatrix.length - 1;
                        }
                    } else {
                        this.trackDrag.dstTrackIdx = -1;
                    }
                }
            };


            dragAndDropBuilder(originalEvent)
            .withScrollableElement(this.$refs.frameAnimatorCanvas)
            .withDraggedElement(this.$refs.trackDragPreview)
            .withDroppableClass('track-droppable-area')
            .onDragStart(() => {
                this.trackDrag.srcTrackIdx = trackIdx;
                this.trackDrag.kind = track.kind;
                this.trackDrag.on = true;
                if (track.kind === 'item') {
                    this.trackDrag.text = track.itemName + ' ' + track.property;
                } else if (track.kind === 'function-header') {
                    this.trackDrag.text = track.name;
                }
            })
            .onDragOver(handleDragOverAndDrop)
            .onDrop((event, trackElement) => {
                handleDragOverAndDrop(event, trackElement);
                if (track.kind === 'item') {
                    if (this.trackDrag.dstTrackIdx < 0) {
                        return;
                    }
                    const dstTrack = this.framesMatrix[this.trackDrag.dstTrackIdx];

                    const originalAnimationIdx = this.findAnimationIndexForTrack(track);
                    let dstAnimationIdx = this.findAnimationIndexForTrack(dstTrack);
                    if (originalAnimationIdx >= 0 && dstAnimationIdx >= 0) {
                        const deletedAnimations = this.framePlayer.shapeProps.animations.splice(originalAnimationIdx, 1);
                        if (!this.trackDrag.dropHead) {
                            dstAnimationIdx += 1;
                        }
                        this.framePlayer.shapeProps.animations.splice(dstAnimationIdx, 0, deletedAnimations[0]);

                        this.updateFramesMatrix();
                        EditorEventBus.schemeChangeCommitted.$emit(this.editorId);
                        this.$forceUpdate();
                    }
                } else if (track.kind === 'function-header') {
                    const dstMatrixTrack = this.framesMatrix[this.trackDrag.dstTrackIdx];
                    if (!dstMatrixTrack) {
                        return;
                    }

                    let dstIdx = -1;
                    let dropAbove = false;

                    if (dstMatrixTrack.kind !== 'function' && dstMatrixTrack.kind !== 'function-header') {
                        dstIdx = 0;
                        dropAbove = true;
                    }

                    const regularAnimations = [];
                    const functionAnimations = [];

                    let srcAnimation = null;

                    forEach(this.framePlayer.shapeProps.animations, animation => {
                        if (animation.kind === 'function') {
                            if (animation.id !== track.id) {
                                functionAnimations.push(animation);
                                if (animation.id === dstMatrixTrack.id) {
                                    dstIdx = functionAnimations.length - 1;
                                    dropAbove = false;
                                }
                            } else {
                                srcAnimation = animation;
                            }
                        } else {
                            regularAnimations.push(animation);
                        }
                    });

                    if (!srcAnimation || dstIdx < 0) {
                        return;
                    }

                    if (dropAbove) {
                        functionAnimations.splice(dstIdx, 0, srcAnimation);
                    } else {
                        functionAnimations.splice(dstIdx + 1, 0, srcAnimation);
                    }

                    this.framePlayer.shapeProps.animations = regularAnimations.concat(functionAnimations);

                    this.updateFramesMatrix();
                    EditorEventBus.schemeChangeCommitted.$emit(this.editorId);
                    this.$forceUpdate();
                }
            })
            .onDone(() => {
                this.trackDrag.on = false;
            })
            .build();
        }
    },
}
</script>